package org.kleathome.dcpu16.emulator;

import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import org.kleathome.dcpu16.DCPU16Instruction;

public class Emulator
{
	private static char[] _memory;
	private static char _registers[] = new char[11];

	public enum Registers
	{
		A, B, C, X, Y, Z, I, J, SP, PC, O;

		public static Registers getRegister(String keyword)
		{
			if (keyword.equalsIgnoreCase("A"))
				return Emulator.Registers.A;
			else if (keyword.equalsIgnoreCase("B"))
				return Emulator.Registers.B; 
			else if (keyword.equalsIgnoreCase("C"))
				return Emulator.Registers.C; 
			else if (keyword.equalsIgnoreCase("X"))
				return Emulator.Registers.X; 
			else if (keyword.equalsIgnoreCase("Y"))
				return Emulator.Registers.Y; 
			else if (keyword.equalsIgnoreCase("Z"))
				return Emulator.Registers.Z; 
			else if (keyword.equalsIgnoreCase("I"))
				return Emulator.Registers.I; 
			else if (keyword.equalsIgnoreCase("J"))
				return Emulator.Registers.J; 
			else if (keyword.equalsIgnoreCase("SP"))
				return Emulator.Registers.SP; 
			else if (keyword.equalsIgnoreCase("PC"))
				return Emulator.Registers.PC; 
			else if (keyword.equalsIgnoreCase("O"))
				return Emulator.Registers.O;
			else
				return null;
		}
		
		public static boolean isRegister(String keyword)
		{
			if (getRegister(keyword) == null)
				return false;
			return true;
		}
	}
	
	public static char getRegister(Registers r)
	{
		//System.out.println("Read Register " + r.toString() + ": " + Integer.toHexString(_registers[r.ordinal()]));
		return _registers[r.ordinal()];
	}

	public static void setRegister(Registers r, char value)
	{
		//System.out.println("Set Register " + r.toString() + ": " + Integer.toHexString(value));
		_registers[r.ordinal()] = value;
	}
	
	private static void init()
	{
		_memory = new char[0x10000];
		for (int i = 0; i < _memory.length; ++i)
			_memory[i] = 0x0000;
		for (int i = 0; i < _registers.length; ++i)
			_registers[i] = 0x0000;
		setStackPointer((char)0xFFFF);
	}
	
	private static boolean isInputASCII(int readBytes, char[] tmp)
	{
		boolean isBinary = false;	// assume ascii object file
		for (int i = 0; i < readBytes; ++i)
		{
			if (Character.isWhitespace(tmp[i]))
				continue;
			if ((tmp[i] < '0' || tmp[i] > '9') && (tmp[i] < 'a' || tmp[i] > 'f') && (tmp[i] < 'A' || tmp[i] > 'F'))
			{
				System.out.println("at " + i + " is " + tmp[i]);
				isBinary = true;
			}
		}
		return !isBinary;
	}
	private static char ASCIItoBinary(char input)
	{
		if (input >= '0' && input <= '9')
			return (char)(input - '0');
		if (input >= 'a' && input <= 'f')
			return (char)(input - 'a' + 10);
		if (input >= 'A' && input <= 'F')
			return (char)(input - 'A' + 10);
		return 0x0000;
	}
	private static char ASCIItoWord(char c1, char c2, char c3, char c4)
	{
		char upperByte = ASCIItoBinary(c1);
		char lowerByte = ASCIItoBinary(c2);
		char upperWord = (char)(upperByte*0x10 + lowerByte);
		upperByte = ASCIItoBinary(c3);
		lowerByte = ASCIItoBinary(c4);
		char lowerWord = (char)(upperByte*0x10 + lowerByte);
		return (char)(upperWord*0x100 + lowerWord);
	}
	private static void readObjectFileIntoMemory(File inFile) throws IOException
	{
		FileReader fr = new FileReader(inFile);
		char[] tmp = new char[0x10000 * 2];
		int readBytes = fr.read(tmp);
		if (isInputASCII(readBytes, tmp))
		{
			int target = 0;
			for (int i = 0; i < readBytes; ++i)
			{
				if (Character.isWhitespace(tmp[i]))
					continue;
				_memory[target] = ASCIItoWord(tmp[i++], tmp[i++], tmp[i++], tmp[i]);
				++target;
			}
		}
		else
		{
			System.out.println("Assuming object file is binary");
			for (int i = 0; i < readBytes; ++i)
				_memory[i] = tmp[i];
		}
	}
	
	public static void runProgram(File inFile)
	{
		init();
		
		try {
			readObjectFileIntoMemory(inFile);

			while (true)
			{
				char pc = getInstructionPointer();
				char instruction = nextInstruction();
				
				if (instruction == 0x0000)
					break;
				DCPU16Instruction op = DCPU16Instruction.getInstruction((char)(instruction & 0x0F));
				op.setA((char)((instruction >> 4)  & 0x3F));
				op.setB((char)((instruction >> 10)  & 0x3F));
				System.out.println(op);
				op.execute();
				int cycles = op.getCycleCost();
				// TODO 100kHz == 100.000 cycles/sec, right now it is 1kHz (1000 cycles/sec)
				//System.out.println("cost: " + cycles);
				Thread.sleep(cycles);
				
				if (pc == getInstructionPointer())
				{
					System.out.println("Possible infinite loop detected");
					break;
				}
			}
		}
		catch (IOException e) {}
		catch (InterruptedException e) {}
	}

	public static char getMemory(char location)
	{
		System.out.println("Accessing memory at [0x" + Integer.toHexString(location) +"]");
		return _memory[location];
	}
	
	public static void setMemory(char location, char value)
	{
		System.out.println("Set memory at [0x" + Integer.toHexString(location) +"] to " + Integer.toHexString(value));
		_memory[location] = value;
	}
	
	public static char nextInstruction()
	{
		char result = _memory[getInstructionPointer()];
		setInstructionPointer((char)(getInstructionPointer() + 1));
		//System.out.println("OPWORD = " + Integer.toBinaryString(result));
		return result;
	}
	public static char getInstructionPointer()
	{
		return getRegister(Registers.PC);
	}
	public static void setInstructionPointer(char val)
	{
		setRegister(Registers.PC, val);
	}
	public static char getStackPointer()
	{
		return getRegister(Registers.SP);
	}
	public static void setStackPointer(char val)
	{
		setRegister(Registers.SP, val);
	}
	public static void push()
	{
		setMemory(getStackPointer(), getRegister(Registers.PC));
		setStackPointer((char)(getStackPointer()-0x01));
	}
	public static void main(String[] args)
	{
		File input = new File(args[0]);
		if (!input.exists())
		{
			System.err.println("Object File " + input.getAbsolutePath() + " not found");
			System.exit(1);
		}
		Emulator.runProgram(input);
	}
}
